// TODO: CODE REVIEW & CLEANUP!
//
// AggregatorFunctions.cs
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using HomeUX.DriverModel;

namespace HomeUX.Components
{

public class ChannelMixerFactory : AggregatorFunctionFactory
{
    MapPoint[] _balanceMap = new MapPoint[] { new MapPoint(0, 0), new MapPoint(0.5, 1), new MapPoint(1, 1) };
    MapPoint[] _faderMap = new MapPoint[] { new MapPoint(0, 0), new MapPoint(0.5, 1), new MapPoint(1, 1) };
    MapPoint[] _reverseBalanceMap, _reverseFaderMap;
    MapPoint[] _frontVolumeMap = new MapPoint[] { new MapPoint(0, 0), new MapPoint(1, 1) };
    MapPoint[] _rearVolumeMap = new MapPoint[] { new MapPoint(0, 0), new MapPoint(1, 1) };

    static readonly string[] BalanceFaderLocationNames =
    {
        // MUST MATCH BalanceFaderLocation enumeration
        "Left",
        "Right",
        "Front",
        "Rear",
        "FrontLeft",
        "FrontRight",
        "RearLeft",
        "RearRight"
        // MUST MATCH BalanceFaderLocation enumeration
    };

    MapPoint[] ReverseMap(MapPoint[] map)
    {
        // |              *          | *
        // |                         |
        // |           *       ==>   |     *
        // |                         |
        // | *  *                    |          *   *
        // +----------------         +----------------
        //   A  B      C  D           1-D 1-C  1-B 1-A

        MapPoint[] result = new MapPoint[map.Length];
        for (int index = 0; index < map.Length; index++)
        {
            MapPoint other = map[map.Length - index - 1];
            result[index] = new MapPoint(1 - other.X, other.Y);
        }
        return result;
    }

    public override void OnInitialize(ConfigurationElement configurationElement)
    {
        string mapString = configurationElement.GetOptionalAttribute("BalanceMap", null);
        if (mapString != null)
            _balanceMap = ParseMap(mapString, configurationElement);

        _reverseBalanceMap = ReverseMap(_balanceMap);

        mapString = configurationElement.GetOptionalAttribute("FaderMap", null);
        if (mapString != null)
            _faderMap = ParseMap(mapString, configurationElement);

        _reverseFaderMap = ReverseMap(_faderMap);

        mapString = configurationElement.GetOptionalAttribute("FrontVolumeMap", null);
        if (mapString != null)
            _frontVolumeMap = ParseMap(mapString, configurationElement);

        mapString = configurationElement.GetOptionalAttribute("RearVolumeMap", null);
        if (mapString != null)
            _rearVolumeMap = ParseMap(mapString, configurationElement);
    }

    public override AggregatorFunction CreateAggregatorFunction(AggregatorDriver aggregator,
        Dictionary<string, string> functionArguments, AggregatorController controller)
    {
        return new ChannelMixerFunction()
        {
            BalanceMap = _balanceMap,
            ReverseBalanceMap = _reverseBalanceMap,
            FaderMap = _faderMap,
            ReverseFaderMap = _reverseFaderMap,
            FrontVolumeMap = _frontVolumeMap,
            RearVolumeMap = _rearVolumeMap,
            Location = ParseEnum<BalanceFaderLocation>(
                GetRequiredArgument(functionArguments, "Location"), BalanceFaderLocationNames),
            Volume = GetOptionalAggregatorPropertyArgument<NumericAggregatorProperty>(aggregator,
                functionArguments, "Volume", controller),
            Balance = GetOptionalAggregatorPropertyArgument<NumericAggregatorProperty>(aggregator,
                functionArguments, "Balance", controller),
            Fader = GetOptionalAggregatorPropertyArgument<NumericAggregatorProperty>(aggregator,
                functionArguments, "Fader", controller)
        };
    }

	/// <summary>
	/// Given a map which defines a function <i>y = f(x)</i> where <i>f</i> is specified as a
	/// series of <i>(x,y)</i> points that are line segment endpoints, this method returns <i>y<i>
	/// for a given <i>x<i> by interpolating along the line segment containing <i>x<i>.
	/// </summary>
	///
	/// <param name="map">The points that define <i>y = f(x)</i>.</param>
	///
	/// <param name="x">The x-value to find the corresponding y-value of.</param>
	///
	/// <remarks>
	/// If <pr>x</pr> is less than the x-value of the first element of <pr>map</pr>, the y-value
	/// of the first element of <pr>map</pr> is returned.  Similarly if <pr>x</pr> is greater than
	/// the x-value of the last element of <pr>map</pr>, the y-value of the last element of
	/// <pr>map</pr> is returned.
	/// </remarks>
	///
	static double Map(MapPoint[] map, double x)
	{
		int  index = Array.BinarySearch<MapPoint>(map, new MapPoint(x, 0));
		if (index >= 0)
		{
			// <x> was found in <map>; return the corresponding y-value
			return map[index].Y;
		}

		// <x> was not found; set <index> to the index of the first element of <map> for which the
		// x-value is larger than <x>
		index = ~index;

		if (index == 0)
		{
			// <x> is smaller than all x-values in <map> -- return the smallest y-value in <map>
			return map[0].Y;
		}

		if (index >= map.Length)
		{
			// <x> is larger than all x-values in <map> -- return the largest y-value in <map>
			return map[map.Length - 1].Y;
		}

		// <x> is between the map[index - 1].X and map[index].X -- interpolate
		MapPoint lower = map[index - 1];
		MapPoint upper = map[index];
		double fraction = (x - lower.X) / (upper.X - lower.X);
		return lower.Y + fraction * (upper.Y - lower.Y);
	}

	static MapPoint[] ParseMap(string mapString, ConfigurationElement element)
	{
        string[] mapPointStrings = mapString.Split(new char[] { ';' },
            StringSplitOptions.RemoveEmptyEntries);
        var mapPoints = new MapPoint[mapPointStrings.Length];

        MapPoint lastPoint = null;
        for (int index = 0; index < mapPointStrings.Length; index++)
        {
            string mapPointString = mapPointStrings[index];
            string[] parts = mapPointString.Split(',');
            if (parts.Length != 2)
            {
                throw new ConfigurationException(element,
                    "Expected a map entry of the form \"number,number\", got: {0}",
                    mapPointString);
            }
            MapPoint thisPoint = new MapPoint(ParseDouble(parts[0], element),
                ParseDouble(parts[1], element));
            mapPoints[index] = thisPoint;

            if (lastPoint != null)
            {
                if (lastPoint.X > thisPoint.X)
                {
                    throw new ConfigurationException(element,
                        "x-values within a map cannot decrease; this rule is violated from ({0},{1}) to ({2},{3})",
                        lastPoint.X, lastPoint.Y, thisPoint.X, thisPoint.Y);
                }
            }

            lastPoint = thisPoint;
        }

        return mapPoints;
	}

    static double ParseDouble(string stringValue, ConfigurationElement element)
    {
        try
        {
            return XmlConvert.ToDouble(stringValue.Trim());
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
                throw new ConfigurationException(element, "Expected a number, got: {0}", stringValue);
            else
                throw;
        }
    }

	class ChannelMixerFunction : AggregatorFunction
	{
        public MapPoint[] BalanceMap;
        public MapPoint[] ReverseBalanceMap;
        public MapPoint[] FaderMap;
        public MapPoint[] ReverseFaderMap;
        public MapPoint[] FrontVolumeMap;
		public MapPoint[] RearVolumeMap;

		public BalanceFaderLocation Location;
		public NumericAggregatorProperty Volume;
		public NumericAggregatorProperty Balance;
		public NumericAggregatorProperty Fader;

		public override object Evaluate()
		{
            double volume = (Volume != null) ? Volume.NativeValue : 0;
            double balance = (Balance != null) ? Balance.NativeValue : 0.5;
            double fader = (Fader != null) ? Fader.NativeValue : 0;

            double result;
            switch (Location)
            {
            case BalanceFaderLocation.Left:
                result = Map(FrontVolumeMap, Map(ReverseBalanceMap, balance) * volume);
                break;
            case BalanceFaderLocation.Right:
                result = Map(FrontVolumeMap, Map(BalanceMap, balance) * volume);
                break;
			case BalanceFaderLocation.Front:
                result = Map(FrontVolumeMap, Map(ReverseFaderMap, fader) * volume);
                break;
			case BalanceFaderLocation.Rear:
                result = Map(RearVolumeMap, Map(FaderMap, fader) * volume);
                break;
			case BalanceFaderLocation.FrontLeft:
                result = Map(FrontVolumeMap, Map(ReverseBalanceMap, balance) * Map(ReverseFaderMap, fader) * volume);
                break;
			case BalanceFaderLocation.FrontRight:
                result = Map(FrontVolumeMap, Map(BalanceMap, balance) * Map(ReverseFaderMap, fader) * volume);
                break;
            case BalanceFaderLocation.RearLeft:
                result = Map(RearVolumeMap, Map(ReverseBalanceMap, balance) * Map(FaderMap, fader) * volume);
                break;
            case BalanceFaderLocation.RearRight:
                result = Map(RearVolumeMap, Map(BalanceMap, balance) * Map(FaderMap, fader) * volume);
                break;
            default:
				throw new InvalidOperationException("Internal Error");
            }

            return Math.Max(0.0, Math.Min(1.0, result));
		}
	}

	public enum BalanceFaderLocation
	{
		// MUST MATCH BalanceFaderLocationNames array
		Left,
		Right,
		Front,
		Rear,
		FrontLeft,
		FrontRight,
		RearLeft,
		RearRight
		// MUST MATCH BalanceFaderLocationNames array
	}

	[DebuggerDisplay("{X},{Y}")]
	class MapPoint : IComparable<MapPoint>
	{
        // immutable
        public double X { get; private set; }
        public double Y { get; private set; }
		public MapPoint(double x, double y)
		{
			X = x;
			Y = y;
		}

        // compares only MapPoint.X, to allow binary search on x-values
		int IComparable<MapPoint>.CompareTo(MapPoint other)
		{
			if (X < other.X)
				return -1;
			if (X > other.X)
				return 1;
			return 0;
		}
	}
}

}
